extern crate bindgen;

use bindgen::Formatter;
use cfg_aliases::cfg_aliases;
use std::cmp::max;
use std::env;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::mem::size_of;
use std::path::Path;
use std::path::PathBuf;

#[cfg(feature = "wide-emacs-int")]
const WIDE_EMACS_INT: bool = true;

#[cfg(not(feature = "wide-emacs-int"))]
const WIDE_EMACS_INT: bool = false;

#[cfg(feature = "ns-impl-gnustep")]
const NS_IMPL_GNUSTEP: bool = true;

#[cfg(not(feature = "ns-impl-gnustep"))]
const NS_IMPL_GNUSTEP: bool = false;

const CFLAGS: &str = "@C_SWITCH_MACHINE@ @C_SWITCH_SYSTEM@ @C_SWITCH_X_SITE@ \
  @GNUSTEP_CFLAGS@ @CFLAGS_SOUND@ @RSVG_CFLAGS@ @IMAGEMAGICK_CFLAGS@ \
  @PNG_CFLAGS@ @LIBXML2_CFLAGS@ @LIBGCCJIT_CFLAGS@ @DBUS_CFLAGS@ \
  @XRANDR_CFLAGS@ @XINERAMA_CFLAGS@ @XFIXES_CFLAGS@ @XDBE_CFLAGS@ \
  @XINPUT_CFLAGS@ @WEBP_CFLAGS@ @WEBKIT_CFLAGS@ @LCMS2_CFLAGS@ \
  @SETTINGS_CFLAGS@ @FREETYPE_CFLAGS@ @FONTCONFIG_CFLAGS@ \
  @HARFBUZZ_CFLAGS@ @LIBOTF_CFLAGS@ @M17N_FLT_CFLAGS@ \
  @LIBSYSTEMD_CFLAGS@ @XSYNC_CFLAGS@ @TREE_SITTER_CFLAGS@ \
  @LIBGNUTLS_CFLAGS@ @NOTIFY_CFLAGS@ @CAIRO_CFLAGS@ \
  @WERROR_CFLAGS@ @HAIKU_CFLAGS@ @XCOMPOSITE_CFLAGS@ @XSHAPE_CFLAGS@ \
  @RUST_CFLAGS@";

fn integer_max_constant(len: usize) -> &'static str {
    match len {
        1 => "0x7F_i8",
        2 => "0x7FFF_i16",
        4 => "0x7FFFFFFF_i32",
        8 => "0x7FFFFFFFFFFFFFFF_i64",
        16 => "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_i128",
        _ => panic!("nonstandard int size {}", len),
    }
}

#[derive(Eq, PartialEq)]
enum ParseState {
    ReadingGlobals,
    ReadingSymbols,
    Complete,
}

fn generate_definitions() {
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    let mut file = File::create(out_path.join("definitions.rs")).unwrap();

    // signed and unsigned size shall be the same.
    let integer_types = [
        ("libc::c_int", "libc::c_uint", size_of::<libc::c_int>()),
        ("libc::c_long", "libc::c_ulong", size_of::<libc::c_long>()),
        (
            "libc::c_longlong",
            "libc::c_ulonglong",
            size_of::<libc::c_longlong>(),
        ),
    ];
    let actual_ptr_size = size_of::<libc::intptr_t>();
    let usable_integers_narrow = ["libc::c_int", "libc::c_long", "libc::c_longlong"];
    let usable_integers_wide = ["libc::c_longlong"];
    let usable_integers = if !WIDE_EMACS_INT {
        usable_integers_narrow.as_ref()
    } else {
        usable_integers_wide.as_ref()
    };
    let integer_type_item = integer_types
        .iter()
        .find(|&&(n, _, l)| {
            actual_ptr_size <= l && usable_integers.iter().find(|&x| x == &n).is_some()
        })
        .expect("build.rs: intptr_t is too large!");

    let float_types = [("f64", size_of::<f64>())];

    let float_type_item = &float_types[0];

    write!(file, "pub type EmacsInt = {};\n", integer_type_item.0).expect("Write error!");
    write!(file, "pub type EmacsUint = {};\n", integer_type_item.1).expect("Write error!");
    write!(
        file,
        "pub const EMACS_INT_MAX: EmacsInt = {};\n",
        integer_max_constant(integer_type_item.2)
    )
    .expect("Write error!");

    write!(
        file,
        "pub const EMACS_INT_SIZE: EmacsInt = {};\n",
        integer_type_item.2
    )
    .expect("Write error!");

    write!(file, "pub type EmacsDouble = {};\n", float_type_item.0).expect("Write error!");
    write!(
        file,
        "pub const EMACS_FLOAT_SIZE: EmacsInt = {};\n",
        max(float_type_item.1, actual_ptr_size)
    )
    .expect("Write error!");

    if NS_IMPL_GNUSTEP {
        write!(file, "pub type BoolBF = libc::c_uint;\n").expect("Write error!");
    } else {
        // There is no such thing as a libc::cbool
        // See https://users.rust-lang.org/t/is-rusts-bool-compatible-with-c99--bool-or-c-bool/3981
        write!(file, "pub type BoolBF = bool;\n").expect("Write error!");
    }

    let bits = 8; // bits in a byte.
    let gc_type_bits = 3;
    let uint_max_len = integer_type_item.2 * bits;
    let int_max_len = uint_max_len - 1;
    let val_max_len = int_max_len - (gc_type_bits - 1);
    let use_lsb_tag = val_max_len - 1 < int_max_len;
    write!(
        file,
        "pub const USE_LSB_TAG: bool = {};\n",
        if use_lsb_tag { "true" } else { "false" }
    )
    .expect("Write error!");
}

fn generate_globals() {
    let in_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
        .join("../../src")
        .join("globals.h");
    let in_file = BufReader::new(File::open(in_path).expect("Failed to open globals file"));

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    let mut out_file = File::create(out_path.join("globals.rs")).unwrap();
    let mut parse_state = ParseState::ReadingGlobals;

    write!(
        out_file,
        "use crate::{{\n    lisp::LispObject,\n    definitions::*,\n    bindings::*,\n}};\n\n"
    )
    .expect("Write error!");
    write!(out_file, "#[allow(unused)]\n").expect("Write error!");
    write!(out_file, "#[repr(C)]\n").expect("Write error!");
    write!(out_file, "pub struct emacs_globals {{\n").expect("Write error!");

    for line in in_file.lines() {
        let line = line.expect("Read error!");
        match parse_state {
            ParseState::ReadingGlobals => {
                if line.starts_with("  ") {
                    let mut parts = line.trim().trim_matches(';').split(' ');
                    let vtype = parts.next().unwrap();
                    let vname = parts.next().unwrap().splitn(2, "_").nth(1).unwrap();

                    write!(
                        out_file,
                        "    pub {}: {},\n",
                        vname,
                        match vtype {
                            "EMACS_INT" => "EmacsInt",
                            "bool_bf" => "BoolBF",
                            "Lisp_Object" => "LispObject",
                            t => t,
                        }
                    )
                    .expect("Write error!");
                }
                if line.starts_with('}') {
                    write!(out_file, "}}\n").expect("Write error!");
                    parse_state = ParseState::ReadingSymbols;
                    continue;
                }
            }

            ParseState::ReadingSymbols => {
                if line.trim().starts_with("#define") {
                    let mut parts = line.split(' ');
                    let _ = parts.next().unwrap(); // The #define
                                                   // Remove the i in iQnil
                    let (_, symbol_name) = parts.next().unwrap().split_at(1);
                    let value = parts.next().unwrap();
                    write!(
                        out_file,
                        "pub const {}: LispObject = crate::lisp::LispObject( \
                         {} * (std::mem::size_of::<Lisp_Symbol>() as EmacsInt));\n",
                        symbol_name, value
                    )
                    .expect("Write error in reading symbols stage");
                } else if line.trim().starts_with("_Noreturn") {
                    parse_state = ParseState::Complete
                }
            }

            ParseState::Complete => {
                break;
            }
        };
    }
}

fn generate_bindings() {
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs");

    let mut builder = bindgen::Builder::default()
        .rust_target(bindgen::RustTarget::Nightly)
        .generate_comments(true);

    builder = builder.clang_arg("-I.");
    builder = builder.clang_arg("-I../../src");
    builder = builder.clang_arg("-I../../lib");

    let cflags_str = CFLAGS;
    let mut processed_args: Vec<String> = Vec::new();
    for arg in cflags_str.split(' ') {
        if arg.starts_with("-I") {
            // we're running clang from a different directory, so we have to adjust any relative include paths
            let path = Path::new("../../src").join(arg.get(2..).unwrap());
            if let Ok(buf) = std::fs::canonicalize(path) {
                processed_args.push(String::from("-I") + &buf.to_string_lossy());
            }
        } else {
            if !arg.is_empty() && !arg.starts_with("-M") && !arg.ends_with(".d") {
                processed_args.push(arg.into());
            }
        };
    }
    builder = builder.clang_args(processed_args);
    if cfg!(target_os = "windows") {
        builder = builder.clang_arg("-I../../nt/inc");
        builder = builder.clang_arg("-Ic:\\Program Files\\LLVM\\lib\\clang\\6.0.0\\include");
    }

    #[cfg(feature = "window-system-pgtk")]
    {
        builder = builder.blocklist_item("GtkWidget");
    }

    builder = builder
        .clang_arg("-Demacs")
        .clang_arg("-DEMACS_EXTERN_INLINE")
        .header("./wrapper.h")
        .generate_inline_functions(true)
        .derive_default(true)
        .ctypes_prefix("::libc")
        .no_copy("winit_output")
        // .blocklist_file("gtk/gtk.h")
        // we define these ourselves, for various reasons
        .blocklist_item("Lisp_Object")
        .blocklist_item("emacs_globals")
        .blocklist_item("Q.*") // symbols like Qnil and so on
        .blocklist_item("USE_LSB_TAG")
        .blocklist_item("VALMASK")
        .blocklist_item("PSEUDOVECTOR_FLAG")
        .blocklist_item("Fmapc")
        // these two are found by bindgen on mac, but not linux
        .blocklist_item("EMACS_INT_MAX")
        .blocklist_item("VAL_MAX")
        // this is wallpaper for a bug in bindgen, we don't lose much by it
        // https://github.com/servo/rust-bindgen/issues/687
        .blocklist_item("BOOL_VECTOR_BITS_PER_CHAR")
        // this is wallpaper for a function argument that shadows a static of the same name
        // https://github.com/servo/rust-bindgen/issues/804
        .blocklist_item("face_change")
        // these never return, and bindgen doesn't yet detect that, so we will do them manually
        .blocklist_item("error")
        .blocklist_item("circular_list")
        .blocklist_item("wrong_type_argument")
        .blocklist_item("nsberror")
        .blocklist_item("emacs_abort")
        .blocklist_item("Fsignal")
        .blocklist_item("memory_full")
        .blocklist_item("wrong_choice")
        .blocklist_item("wrong_range")
        // these are defined in data.rs
        // .blocklist_item("Lisp_Fwd")
        // .blocklist_item("Lisp_.*fwd")
        // these are defined in remacs_lib
        .blocklist_item("timespec")
        .blocklist_item("fd_set")
        .blocklist_item("pselect")
        .blocklist_item("sigset_t")
        .blocklist_item("timex")
        .blocklist_item("clock_adjtime")
        // by default we want C enums to be converted into a Rust module with constants in it
        .default_enum_style(bindgen::EnumVariation::ModuleConsts)
        // enums with only one variant are better as simple constants
        .constified_enum("EMACS_INT_WIDTH")
        .constified_enum("BOOL_VECTOR_BITS_PER_CHAR")
        .constified_enum("BITS_PER_BITS_WORD")
        // TODO(db48x): verify that these enums meet Rust's requirements (primarily that they have no duplicate variants)
        .rustified_enum("Lisp_Misc_Type")
        .rustified_enum("Lisp_Type")
        .rustified_enum("case_action")
        .rustified_enum("face_id")
        .rustified_enum("output_method")
        .rustified_enum("pvec_type")
        .rustified_enum("symbol_redirect")
        .rustified_enum("syntaxcode")
        .rustified_enum("VTermProp")
        .rustified_enum("VTermColorType");

    if cfg!(target_os = "windows") {
        builder = builder
            .rustified_enum("_HEAP_INFORMATION_CLASS")
            .rustified_enum("SECURITY_IMPERSONATION_LEVEL")
            .rustified_enum("TOKEN_INFORMATION_CLASS");
    }

    let bindings = builder
        .formatter(Formatter::Rustfmt)
        .rustfmt_configuration_file(std::fs::canonicalize("rustfmt.toml").ok())
        .generate()
        .expect("Unable to generate bindings");

    // https://github.com/rust-lang-nursery/rust-bindgen/issues/839
    let source = bindings.to_string();
    let re = regex::Regex::new(
        r"pub use self\s*::\s*gnutls_cipher_algorithm_t as gnutls_cipher_algorithm\s*;",
    );
    let munged = re.unwrap().replace_all(&source, "");
    let mut file = File::create(out_path).unwrap();
    write!(file, "use crate::{{\n    globals::emacs_globals,\n    sys::Lisp_Object,\n}};\n\nuse libc::{{timespec, fd_set, sigset_t}};\n\n#[cfg(feature = \"window-system-pgtk\")]\nuse gtk_sys::GtkWidget;\n").expect("Write error!");
    file.write_all(munged.into_owned().as_bytes()).unwrap();
}

fn main() {
    println!("cargo:rerun-if-changed=Cargo.toml");
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=../../src/globals.h");
    println!("cargo:rerun-if-changed=./wrapper.h");

    generate_definitions();
    generate_globals();
    generate_bindings();

    // Setup cfg aliases
    cfg_aliases! {
        android_platform: { target_os = "android" },
        wasm_platform: { target_arch = "wasm32" },
        macos_platform: { target_os = "macos" },
        ios_platform: { target_os = "ios" },
        windows_platform: { target_os = "windows" },
        apple: { any(target_os = "ios", target_os = "macos") },
        free_unix: { all(unix, not(apple), not(android_platform)) },

        x11_platform: { all(feature = "x11", free_unix, not(wasm), use_winit)},
        wayland_platform: { all(feature = "wayland", free_unix, not(wasm), use_winit) },

        // Emacs
            use_webrender: { feature = "webrender"},
        have_ntgui: { feature = "window-system-w32" },
        have_pgtk: { feature = "window-system-pgtk" },
        have_x11: { feature = "window-system-x11" },
        have_ns: { feature = "window-system-nextstep" },
        have_haiku: { feature = "window-system-haiku"},
        have_android: { feature = "window-system-android" },
        have_winit: { feature = "window-system-winit" },
    surfman: { feature = "surfman" },
        glutin: { feature = "glutin" },
        gtk3: { all(feature = "gtk3", have_pgtk) },
        have_window_system: {
        any(have_ntgui, have_pgtk, have_x11, have_ns, have_haiku, have_android, have_winit)},

    }
}
